﻿using System;
using System.Collections.Generic;
using System.Text;
using Vortex.Wrapper.Direct3D;
using Vortex.Debugging;
using Vortex.Drawing.Performance;
using Vortex.Wrapper;

namespace Vortex.Drawing {
	/// <summary>
	/// It is an implementation of 2D renderer for Canvas object
	/// </summary>
	class Renderer2D : Component, IDisposable {
		private static readonly string VIEW_TRANSFORM = "VIEW_TRANSFORM";
		private static readonly string VIEW_COLORTINT = "VIEW_COLORTINT";

		///<summary>Contains subpixel correction to provide texel to pixel correction</summary>
		private readonly Vector2 TEXEL_TO_PIXEL_CORRECTION = Vector2.Unit.Half.Negative;

		//private PrelockedVertexStream _vertexStream;
		private BufferedVertexStream _vertexStream;
		private Canvas2D _currentCanvas;

		private CanvasStates _states = new CanvasStates();
		private SamplerState[] _samplerStates = new SamplerState[CanvasStates.MAX_TEXTURE_COUNT];

		private VertexShader _defaultVertexShader;
		private PixelShader _defaultPixelShader;
		private Texture _blankTexture;

		public Renderer2D(DrawingDevice device) : base(device) {
			//_vertexStream = new PrelockedVertexStream(device);
			_vertexStream = new BufferedVertexStream(device);
		}

		/// <summary>
		/// Initializes this 2D renderer... Loads default shader or so.
		/// </summary>
		public void Initialize() {
			_defaultVertexShader = new VertexShader(@"Vortex.Drawing.Files.default.vsh");
			_defaultPixelShader = new PixelShader(@"Vortex.Drawing.Files.default.psh");
			_blankTexture = new Texture(@"Vortex.Drawing.Files.1x1.png", PixelFormat.Default);
			
			//load other globals
			Canvas2D.LoadLineStrokes();
			SpriteFont.LoadGlobals();
		}

		/// <summary>
		/// Destroys the renderer component
		/// </summary>
		public override void Destroy() {
			//free other globals just before
			Canvas2D.FreeLineStrokes();
            SpriteFont.FreeGlobals();
            
			_vertexStream.Destroy();
            _blankTexture.Dispose();
			_defaultVertexShader.Dispose();
			_defaultPixelShader.Dispose();
		}

		#region Public interface

		/// <summary>
		/// Begins the 2D scene.
		/// </summary>
		public void BeginScene() {
			SetDefaultStates();
			_vertexStream.Open();
			_currentCanvas = null;
		}

		/// <summary>
		/// Ends the 2D scene.
		/// </summary>
		public void EndScene() {
			_vertexStream.Close();
		}

		/// <summary>
		/// Flushes rendering queue.
		/// </summary>
		public void Flush() {
			//check before call
			if (!_vertexStream.IsEmpty) {				
				_vertexStream.Flush();
			}
		}

		/// <summary>
		/// Allocates the vertex + index fragment to renderer queue.
		/// </summary>
		/// <param name="fragment">The fragment signature.</param>
		/// <param name="location">The location of fragment in buffer.</param>
		public void AllocateFragment(VertexFragment fragment, out FragmentLocation location) {
			_vertexStream.AllocateFragment(fragment, out location);
		}

		/// <summary>
		/// Sets the batch size hint for this renderer.
		/// </summary>
		/// <param name="batchSizeHint">The batch size hint.</param>
		public void SetBatchSizeHint(int batchSizeHint) {
			Flush();
			_vertexStream.SetBatchSizeHint(batchSizeHint);
		}

		/// <summary>
		/// Sets the current canvas for renderer.
		/// </summary>
		/// <param name="canvas">The canvas instance.</param>
		public void SetCanvas(Canvas2D canvas) {
			if (canvas != _currentCanvas) {
				ChangeCanvas(canvas);
			}
			//any way we need to sync canvas
			if (canvas.HasStateChanges) {
				Flush();
				SyncStates(canvas);
			}
		}

		/// <summary>
		/// Sets the current canvas for renderer and update primary texture.
		/// </summary>
		/// <param name="canvas">The canvas instance.</param>
		public void SetCanvas(Canvas2D canvas, BaseTexture texture) {
			if (canvas != _currentCanvas) {
				ChangeCanvas(canvas);
			}
			//any way we need to sync canvas
			if (canvas.HasStateChanges) {
				Flush();
				SyncStates(canvas);
			}
			//check texture is changed?
			texture = texture ?? _blankTexture;
			if (texture != _states.PrimaryTexture) {
				Flush();
				SetTexture(CanvasStates.PRIMARY_TEXTURE_INDEX, texture ?? _blankTexture);
			}
		}

		#endregion

		#region Main Private Methods

		/// <summary>
		/// Sets the default states for device.
		/// </summary>
		private void SetDefaultStates() {
			_d3dDevice.SetRenderState(D3DRenderState.LIGHTING, false);
			_d3dDevice.SetRenderState(D3DRenderState.DITHERENABLE, true);
			_d3dDevice.SetRenderState(D3DRenderState.SPECULARENABLE, false);
			_d3dDevice.SetRenderState(D3DRenderState.ALPHABLENDENABLE, true);
			_d3dDevice.SetRenderState(D3DRenderState.CULLMODE, 1);
			_d3dDevice.SetRenderState(D3DRenderState.MULTISAMPLEANTIALIAS, true);

			//set default data
			SetVertexShader(_defaultVertexShader);
			SetPixelShader(_defaultPixelShader);
			SetBlending(Blending.Normal);
			SetAlphaTest(AlphaTest.Always);
			SetZEffect(ZEffect.None);
			SetClipping(Clipping.None);

			//set up sampler states 
			for (int n = 0; n < CanvasStates.MAX_TEXTURE_COUNT; ++n) {
				SetTexture(n, _blankTexture);
				SetTextureFilter(n, TextureFilter.Linear);
				SetTextureAddressModeU(n, TextureAddressMode.Wrap);
				SetTextureAddressModeV(n, TextureAddressMode.Wrap);
				SetTextureBorderColor(n, ColorU.White);
			}

			_d3dDevice.DebugViewport();
		}

		private void ChangeCanvas(Canvas2D canvas) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.CanvasSwitches);
#endif
			//1. Ensure everything is rendered from previous canvas
			Flush();
			//2. Set new canvas
			_currentCanvas = canvas;
			//4. Change device render target
			ChangeTarget(canvas.Target);
			//3. Sync canvas states
			canvas.StateChanges = CanvasStateChanges.All;
			SyncStates(canvas);
		}

		/// <summary>
		/// Changes the render target.
		/// </summary>
		/// <param name="drawingTarget">The drawing target.</param>
		private void ChangeTarget(IDrawingTarget drawingTarget) {
			TargetData targetData = drawingTarget.TargetData;
			
			_d3dDevice.SetRenderTarget(0, targetData.BackBuffer);
			_d3dDevice.SetDepthStencilSurface(targetData.ZBuffer);
		}

		/// <summary>
		/// Synchronizes the canvas & renderer.
		/// </summary>
		/// <param name="canvas">The canvas.</param>
		private void SyncStates(Canvas2D canvas) {
			CanvasStateChanges changes = canvas.StateChanges;
			CanvasStates states = canvas.States;

			if ((changes & CanvasStateChanges.AnyVertexShader) != CanvasStateChanges.None) {
				VertexShader shader = states.VertexShader ?? _defaultVertexShader;
				if (shader != _states.VertexShader || (changes & CanvasStateChanges.ForceVertexShader) != CanvasStateChanges.None) {
					SetVertexShader(shader);
				}
			}

			if ((changes & CanvasStateChanges.PixelShader) != CanvasStateChanges.None) {
				PixelShader shader = states.PixelShader ?? _defaultPixelShader;
				if (shader != _states.PixelShader) {
					SetPixelShader(shader);
				}
			}

			if ((changes & CanvasStateChanges.Blending) != CanvasStateChanges.None) {
				Blending blending = states.Blending;
				if (!Blending.Equals(blending, _states.Blending)) {
					SetBlending(blending);
				}
			}

			if ((changes & CanvasStateChanges.AlphaTest) != CanvasStateChanges.None) {
				AlphaTest test = states.AlphaTest;
				if (test != _states.AlphaTest) {
					SetAlphaTest(test);
				}
			}

			if ((changes & CanvasStateChanges.ZEffect) != CanvasStateChanges.None) {
				ZEffect effect = states.ZEffect;
				if (effect != _states.ZEffect) {
					SetZEffect(effect);
				}
			}

			if ((changes & CanvasStateChanges.Clipping) != CanvasStateChanges.None) {
				Clipping clipping = states.Clipping;
				if (clipping != null || _states.Clipping != null) {
					SetClipping(clipping);
				}
			}

			//check texture changes
			if ((changes & CanvasStateChanges.AnyTexture) != CanvasStateChanges.None) {

				if ((changes & CanvasStateChanges.PrimaryTexture) != CanvasStateChanges.None) {
					BaseTexture texture = states.PrimaryTexture ?? _blankTexture;
					if (texture != _states.PrimaryTexture) {
						SetTexture(CanvasStates.PRIMARY_TEXTURE_INDEX, texture);
					}
				}

				if ((changes & CanvasStateChanges.BrushTexture) != CanvasStateChanges.None) {
					BaseTexture texture = states.BrushTexture ?? _blankTexture;
					if (texture != _states.BrushTexture) {
						SetTexture(CanvasStates.BRUSH_TEXTURE_INDEX, texture);
					}
				}

				if ((changes & CanvasStateChanges.CustomTextures) != CanvasStateChanges.None) {
					//compare all of custom textures in loop
					for (int n = 2; n < CanvasStates.MAX_TEXTURE_COUNT; ++n) {
						BaseTexture texture = states.Textures[n] ?? _blankTexture;
						if (texture != _states.Textures[n]) {
							SetTexture(n, texture);
						}
					}
				}
			}

			canvas.StateChanges = CanvasStateChanges.None;
		}

		/// <summary>
		/// Creates the view transform based on region.
		/// </summary>
		/// <param name="rect">The rect.</param>
		/// <param name="transform">The transform.</param>
		private void MakeViewTransform(ref Rect region, out Transform2D transform) {
			//Log.Success("REGION: {0}", region);
			Vector2 offset = -region.Center + TEXEL_TO_PIXEL_CORRECTION;
			Vector2 scale = new Vector2(region.HalfWidth, -region.HalfHeight).Inverted;
			transform = Transform2D.Empty.Translate(offset).Scale(scale);
		}

		#endregion

		#region Set State Methods

		/// <summary>
		/// Updates the logical view transformation.
		/// </summary>
		/// <param name="rect">The canvas logical view region.</param>
		private void UpdateLogicalView(Rect rect) {
			Transform2D transform;
			MakeViewTransform(ref rect, out transform);
			_states.VertexShader.Params.Set("VIEW_TRANSFORM", transform);			
		}

		/// <summary>
		/// Sets the vertex shader as current.
		/// </summary>
		/// <param name="vertexShader">The vertex shader.</param>
		private void SetVertexShader(VertexShader vertexShader) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.VertexShaderChanged);
#endif

			_states.VertexShader = vertexShader;

			//set canvas transform
			if (null != _currentCanvas) {
				UpdateLogicalView(_currentCanvas.Region);
			}

			_d3dDevice.SetVertexShader(vertexShader.Handle);			
			vertexShader.Params.RefineCachedConstants();			
			
		}

		/// <summary>
		/// Sets the pixel shader as current.
		/// </summary>
		/// <param name="vertexShader">The vertex shader.</param>
		private void SetPixelShader(PixelShader pixelShader) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.PixelShaderChanged);
#endif
			_d3dDevice.SetPixelShader(pixelShader.Handle);
			pixelShader.Params.RefineCachedConstants();
			_states.PixelShader = pixelShader;
		}

		/// <summary>
		/// Sets texture with specified index for renderer.
		/// </summary>
		/// <param name="index">The sampler index.</param>
		/// <param name="texture">The texture.</param>
		private void SetTexture(int index, BaseTexture texture) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.TexturesChanged);
#endif

			_d3dDevice.SetTexture((uint)index, texture.ApiRef);
			_states.Textures[index] = texture;

			//check texture options to sync with renderer
			TextureOptions options = texture.Options;

			if (options.Filter != _samplerStates[index].Filter) {
				SetTextureFilter(index, options.Filter);
			}
			if (options.AddressModeU != _samplerStates[index].AddressModeU) {
				SetTextureAddressModeU(index, options.AddressModeU);
			}
			if (options.AddressModeV != _samplerStates[index].AddressModeV) {
				SetTextureAddressModeV(index, options.AddressModeV);
			}
			if (options.BorderColor != _samplerStates[index].BorderColor) {
				SetTextureBorderColor(index, options.BorderColor);
			}
		}

		/// <summary>
		/// Sets the blending for this renderer.
		/// </summary>
		/// <param name="blending">The blending.</param>
		private void SetBlending(Blending blending) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.StatesChanged);
#endif
			_d3dDevice.SetRenderState(D3DRenderState.SRCBLEND, (uint)blending.SourceFactor);
			_d3dDevice.SetRenderState(D3DRenderState.DESTBLEND, (uint)blending.DestFactor);
			_d3dDevice.SetRenderState(D3DRenderState.BLENDOP, (uint)blending.Operation);
			_states.Blending = blending;
		}

		/// <summary>
		/// Sets the alpha test for this renderer.
		/// </summary>
		/// <param name="test">The test.</param>
		private void SetAlphaTest(AlphaTest test) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.StatesChanged);
#endif
			if (TestFunction.Always == test.Function) {
				_d3dDevice.SetRenderState(D3DRenderState.ALPHATESTENABLE, false);
			} else {
				_d3dDevice.SetRenderState(D3DRenderState.ALPHATESTENABLE, true);
				_d3dDevice.SetRenderState(D3DRenderState.ALPHAFUNC, (uint)test.Function);
				_d3dDevice.SetRenderState(D3DRenderState.ALPHAREF, (uint)test.RefValue);
			}
			_states.AlphaTest = test;
		}

		/// <summary>
		/// Sets the Z-Effect for this renderer.
		/// </summary>
		/// <param name="test">The test.</param>
		private void SetZEffect(ZEffect effect) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.StatesChanged);
#endif
			if (TestFunction.Always == effect.Function && !effect.WriteZ) {
				_d3dDevice.SetRenderState(D3DRenderState.ZENABLE, false);
			} else {
				_d3dDevice.SetRenderState(D3DRenderState.ZENABLE, true);
				_d3dDevice.SetRenderState(D3DRenderState.ZWRITEENABLE, effect.WriteZ);
				_d3dDevice.SetRenderState(D3DRenderState.ZFUNC, (uint)effect.Function);
			}

			_states.ZEffect = effect;
		}

		/// <summary>
		/// Sets the clipping fr renderer. Clipping state differs a little bit from other states.
		/// It is canvas dependent, it means we have some canvas region and clipping is relative to it, but device understands only pixel units.
		/// </summary>
		/// <param name="clipping">The clipping.</param>
		private void SetClipping(Clipping clipping) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.StatesChanged);
#endif
			if (null != clipping) {
				_d3dDevice.SetRenderState(D3DRenderState.SCISSORTESTENABLE, true);
				_d3dDevice.SetScissorRect(clipping.ClippingRect.ToRectangle());
			} else {
				_d3dDevice.SetRenderState(D3DRenderState.SCISSORTESTENABLE, false);
			}

			_states.Clipping = clipping;
		}

		/// <summary>
		/// Sets the texture filter to specified sampler.
		/// </summary>
		/// <param name="samplerIndex">Index of the sampler.</param>
		/// <param name="filter">The filter.</param>
		private void SetTextureFilter(int samplerIndex, TextureFilter filter) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.SamplerStatesChanged);
#endif
			_samplerStates[samplerIndex].Filter = filter;
			_d3dDevice.SetSamplerState((uint)samplerIndex, D3DSamplerState.MAGFILTER, (uint)filter);
			_d3dDevice.SetSamplerState((uint)samplerIndex, D3DSamplerState.MINFILTER, (uint)filter);
			_d3dDevice.SetSamplerState((uint)samplerIndex, D3DSamplerState.MIPFILTER, (uint)filter);
		}

		/// <summary>
		/// Sets the texture address mode for U axis.
		/// </summary>
		/// <param name="samplerIndex">Index of the sampler.</param>
		/// <param name="addressMode">The address mode.</param>
		private void SetTextureAddressModeU(int samplerIndex, TextureAddressMode addressMode) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.SamplerStatesChanged);
#endif
			_samplerStates[samplerIndex].AddressModeU = addressMode;
			_d3dDevice.SetSamplerState((uint)samplerIndex, D3DSamplerState.ADDRESSU, (uint)addressMode);
		}

		/// <summary>
		/// Sets the texture address mode for V axis.
		/// </summary>
		/// <param name="samplerIndex">Index of the sampler.</param>
		/// <param name="addressMode">The address mode.</param>
		private void SetTextureAddressModeV(int samplerIndex, TextureAddressMode addressMode) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.SamplerStatesChanged);
#endif
			_samplerStates[samplerIndex].AddressModeV = addressMode;
			_d3dDevice.SetSamplerState((uint)samplerIndex, D3DSamplerState.ADDRESSV, (uint)addressMode);
		}

		/// <summary>
		/// Sets the color of the texture border for sampler.
		/// </summary>
		/// <param name="samplerIndex">Index of the sampler.</param>
		/// <param name="borderColor">Color of the border.</param>
		private void SetTextureBorderColor(int samplerIndex, ColorU borderColor) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.SamplerStatesChanged);
#endif
			_samplerStates[samplerIndex].BorderColor = borderColor;
			_d3dDevice.SetSamplerState((uint)samplerIndex, D3DSamplerState.BORDERCOLOR, (uint)borderColor);
		}

		#endregion

		/// <summary>
		/// Notifies renderer that shader parameters were changed.
		/// </summary>
		/// <param name="shader">The shader for which .</param>
		/// <returns>true if shader is currently in use, false if it is not</returns>
		public bool ShaderParametersChanged(Shader shader) {
#if PERF_STATS
			Device.PerfMonitor.AddCounter(DeviceCounter.ShaderConstantsChanged);
#endif
			if (_states.VertexShader == shader || _states.PixelShader == shader) {
				//need to flush all geometry before params will be sent to device
#if PERF_STATS
				Device.PerfMonitor.AddCounter(DeviceCounter.ShaderConstantsInterruptedBatch);
#endif
				Flush();
				return true;
			} else {
				return true;
			}
		}


		/// <summary>
		/// Sets the texture filter for all samplers where this texture is binded.
		/// </summary>
		/// <param name="texture">The texture.</param>
		/// <param name="filter">The filter.</param>
		public void SetTextureFilter(BaseTexture texture, TextureFilter filter) {
			for (int n = 0; n < CanvasStates.MAX_TEXTURE_COUNT; ++n) {
				if (_states.Textures[n] == texture) {
					Flush();
					SetTextureFilter(n, filter);
				}
			}
		}

		/// <summary>
		/// Sets the texture address mode for U axis for all samplers where this texture is binded.
		/// </summary>
		/// <param name="texture">The texture.</param>
		/// <param name="addressMode">The address mode.</param>
		public void SetTextureAddressModeU(BaseTexture texture, TextureAddressMode addressMode) {
			for (int n = 0; n < CanvasStates.MAX_TEXTURE_COUNT; ++n) {
				if (_states.Textures[n] == texture) {
					Flush();
					SetTextureAddressModeU(n, addressMode);
				}
			}
		}

		/// <summary>
		/// Sets the texture address mode for V axis for all samplers where this texture is binded.
		/// </summary>
		/// <param name="texture">The texture.</param>
		/// <param name="addressMode">The address mode.</param>
		public void SetTextureAddressModeV(BaseTexture texture, TextureAddressMode addressMode) {
			for (int n = 0; n < CanvasStates.MAX_TEXTURE_COUNT; ++n) {
				if (_states.Textures[n] == texture) {
					Flush();
					SetTextureAddressModeV(n, addressMode);
				}
			}
		}

		/// <summary>
		/// Sets the texture border color for all samplers where this texture is binded.
		/// </summary>
		/// <param name="texture">The texture.</param>
		/// <param name="borderColor">Color of the border.</param>
		public void SetTextureBorderColor(BaseTexture texture, ColorU borderColor) {
			for (int n = 0; n < CanvasStates.MAX_TEXTURE_COUNT; ++n) {
				if (_states.Textures[n] == texture) {
					Flush();
					SetTextureBorderColor(n, borderColor);
				}
			}
		}

		/// <summary>
		/// Draws the vertex buffer.
		/// </summary>
		/// <param name="buffer">The vertex buffer.</param>
		/// <param name="transform">The transform to apply to buffer content.</param>
		/// <param name="colorTint">The color tint to apply.</param>
		public void DrawVertexBuffer(VertexBuffer buffer, Transform2D transform, ColorU colorTint) {
			Transform2D screenTransform;
			Rect canvasRect = _currentCanvas.Region;
			MakeViewTransform(ref canvasRect, out screenTransform);

			unsafe {
				//close vertex stream...
				_vertexStream.Close();

				ShaderParamCollection shaderParams = _states.VertexShader.Params;
				bool hasTransform = shaderParams.Contains(VIEW_TRANSFORM);
				bool hasColorTint = shaderParams.Contains(VIEW_COLORTINT);

				//set view transform
				if (hasTransform) shaderParams.Set(VIEW_TRANSFORM, transform * screenTransform);
				if (hasColorTint) shaderParams.Set(VIEW_COLORTINT, colorTint);

				//set vertex source
				_d3dDevice.SetStreamSource(0, buffer.ApiRef, 0, (uint)sizeof(Vertex2D));
				_d3dDevice.DrawPrimitive((D3DPrimitiveType)buffer.PrimitiveType, 0, (uint)BufferedVertexStream.CalculatePrimitiveCount(buffer.PrimitiveType, buffer.VertexCount));

				if (hasTransform) shaderParams.Set(VIEW_TRANSFORM, screenTransform);
				if (hasColorTint) shaderParams.Set(VIEW_COLORTINT, ColorU.Blank);

				//vertex stream should be reopened again after
				_vertexStream.Open();
			}
		}
	}
}
